package com.nuoniu.sibanyun.config;

import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.nuoniu.sibanyun.authException.TokenErrorException;
import com.nuoniu.sibanyun.common.exception.NuoNiuException;
import com.nuoniu.sibanyun.entity.SysUser;
import com.nuoniu.sibanyun.service.ISysMenuService;
import com.nuoniu.sibanyun.service.ISysUserService;
import com.nuoniu.sibanyun.service.redis.RedisService;
import com.nuoniu.sibanyun.userThread.UserInfo;
import io.jsonwebtoken.*;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.http.HttpServletRequest;
import java.util.*;

@Component
@Log4j2
public class JwtUtils {

    public static final String TOKEN_HEADER = "Access-Token";
    public static final String TOKEN_PREFIX = "Bearer ";

    public static final String SUBJECT = "sibanyun";
    /**
     * PC端 token 过期时间
     */
    public static final  long JWT_EXPIRE_TIME =  100 * 60; // 100分钟
    public static final long JWT_EXPIRE_TIME_LONG = 1000 * JWT_EXPIRE_TIME;
    /**
     * 移动端 token 过期时间
     */
    public static final  long JWT_EXPIRE_TIME_MOBILE =  60 * 60 * 24 * 180; //  180天
//    public static final  long JWT_EXPIRE_TIME_MOBILE =  60 * 1; //  180天
    public static final long JWT_EXPIRE_TIME_LONG_MOBILE = 1000 * JWT_EXPIRE_TIME_MOBILE;

    /**
     * 秘钥
     */
    public static final String APPSECRET_KEY = "sibanyun_secret";

    public static final String ROLE_CLAIMS = "rol";

    public static final String REDIS_USER_AUTHORITIES_PREFIX = "USER_AUTHORITIES_";

    /**
     * 用户基本信息前缀
     */
    public static final String REDIS_USER_NAME_PREFIX = "SBY_USER_INFO_";
    /**
     * PC端 用户Token前缀
     */
    public static final String USER_JWT_TOKEN_PREFIX = "UserJwtToken_";
    /**
     * 移动端 用户Token前缀
     */
    public static final String USER_JWT_TOKEN_PREFIX_MOBILE = "MobileUserJwtToken_";
    /**
     * 老Token 过期时间
     */
    public static final long OLD_TOKEN_EXPIRE =  10 ;            //10秒
    public static final long OLD_TOKEN_EXPIRE_TIME = 1000 * OLD_TOKEN_EXPIRE;
    /**
     * token的剩余有效时间
     */
    public static final long ADVANCE_EXPIRE = 60 * 30;
    public static final long ADVANCE_EXPIRE_TIME = 1000 * ADVANCE_EXPIRE;

    /**
     * token的剩余有效时间
     */
    public static final long USE_MOBILE = 60 * 60 * 24 * 150;       //  移动端 使用时长 （150天）
//    public static final long USE_MOBILE = 30;       //  移动端 使用时长 （150天）
    public static final long USE_MOBILE_TIME = 1000 * USE_MOBILE;   //  移动端 使用时长
    public static final long ADVANCE_EXPIRE_MOBILE_TIME = JWT_EXPIRE_TIME_LONG_MOBILE - USE_MOBILE_TIME;

    public static final String jwtVersion = "0.7.0";

    @Autowired
    private RedisService redisService;

    @Autowired
    private ISysUserService sysUserService;

    @Autowired
    private ISysMenuService sysMenuService;


    /**
     * 从token中 获取用户
     */
    public String getUserFromToken(String token) {
        return getClaimFromToken(token).getSubject();
    }



    /**
     * 获取可用的token
     * 如该用户当前token可用，即返回
     * 当前token不可用，则返回一个新token
     * @param token
     * @return
     */
    public String getGoodToken(String token,boolean isMobile){
        String user_jwt_token_prefix = USER_JWT_TOKEN_PREFIX;
        long advance_expire_time = ADVANCE_EXPIRE_TIME;
        if (isMobile){
            user_jwt_token_prefix = USER_JWT_TOKEN_PREFIX_MOBILE;
            advance_expire_time = ADVANCE_EXPIRE_MOBILE_TIME;

        }
        Claims body = this.checkToken(token);
        String userName = body.getSubject();
        long expireTime = body.getExpiration().getTime();
        long diff = expireTime - System.currentTimeMillis();
        log.info("*** 验证token的有效性 *** 1.是否移动端：{},token前缀：{},剩余时间：{}毫秒",isMobile,user_jwt_token_prefix,diff);
        //如果有效期小于xx时间，则不建议继续使用该token
        if (diff < advance_expire_time) {
            Object tokenObj = redisService.get(user_jwt_token_prefix + userName);
            String redisToken = tokenObj != null ? tokenObj.toString() : "" ;
//            log.info("传入的token：{},redisToken：{}",token,redisToken);
            if(token.equals(redisToken)){
                String newToken = this.createToken(userName,isMobile);
                //初始化新token
                this.initNewToken(userName, newToken,isMobile,false);
                return newToken;
            }else{
                log.info("*** 验证token的有效性 *** 2.用户：{}_返回之前刷新的token:{}",userName,redisToken);
                return  redisToken;
            }
        }else{
            return token;
        }
    }

    /**
     * 检查当前token是否还能继续使用
     * true：可以  false：不建议
     * @param token
     * @return
     */
    public Claims checkToken(String token){
        SecretKey secretKey = this.createSecretKey();
        Jws<Claims> jws;
        Claims body;
        //1.获取传入的真实token
        String jwtToken = token.replace("jwt_", "");
        log.info("*** 校验并解析token ***：{}",jwtToken);
        //2.传入的真实token 的秘钥 校验 和 过期时间校验
        try {
            jws = Jwts.parser()   //得到DefaultJwtParser
                    .setSigningKey(secretKey)  //设置签名的秘钥
                    .parseClaimsJws(jwtToken);
        }catch (Exception e){
            log.info("JWT校验失败:{}", e.getMessage());
            throw new TokenErrorException("令牌失效,请重新登陆");
        }
        //3.用户名 不能为空
        body = jws.getBody();
        String userName = body.getSubject();
        if(StringUtils.isBlank(userName)){
            log.info("令牌校验：用户名不能为空");
            throw new NuoNiuException("令牌失效,请重新登陆");
        }
        //4.获取当前token是否有效（redis验证）
        Object nameObj = redisService.get(token);
        String name = nameObj != null ? nameObj.toString() : "" ;
        if(StringUtils.isBlank(userName) || !userName.equals(name)){
            log.info("令牌校验：Redis Token 失效:{}",token);
            throw new TokenErrorException("令牌失效,请重新登陆");
        }
//        //5.传入的token 和  本地redis存放的 token 进行比较
//        Object tokenObj = redisService.get(USER_JWT_TOKEN_PREFIX + userName);
//        String redisToken = tokenObj != null ? tokenObj.toString() : "" ;
//        if(!token.equals(redisToken)){
//            log.debug("用户名：{},传入的token：{},redis存放的token：",userName,token,redisToken);
//            throw new TokenErrorException("令牌失效,请重新登陆" );
//        }
        return body;
    }



    /**
     * 创建新token,（刷新 和登录时 使用）
     * @param userName 用户名
     * @return
     */
    public String createToken(String userName,boolean isMobile){
//        Map<String,Object> map = new HashMap<>();
//        map.put(ROLE_CLAIMS, role);

        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; //指定签名的时候使用的签名算法，也就是header那部分，jjwt已经将这部分内容封装好了。
        long nowMillis = System.currentTimeMillis();//生成JWT的时间
        Date now = new Date(nowMillis);
//        Map<String,Object> claims = new HashMap<String,Object>();//创建payload的私有声明（根据特定的业务需要添加，如果要拿这个做验证，一般是需要和jwt的接收方提前沟通好验证方式的）
        SecretKey secretKey = createSecretKey();//生成签名的时候使用的秘钥secret,这个方法本地封装了的，一般可以从本地配置文件中读取，切记这个秘钥不能外露哦。它就是你服务端的私钥，在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
        //下面就是在为payload添加各种标准声明和私有声明了
        JwtBuilder builder = Jwts.builder() //这里其实就是new一个JwtBuilder，设置jwt的body
//                .setClaims(map)          //如果有私有声明，一定要先设置这个自己创建的私有的声明，这个是给builder的claim赋值，一旦写在标准的声明赋值之后，就是覆盖了那些标准的声明的
                .setId(UUID.randomUUID().toString())                  //设置jti(JWT ID)：是JWT的唯一标识，根据业务需要，这个可以设置为一个不重复的值，主要用来作为一次性token,从而回避重放攻击。
                .setIssuedAt(now)           //iat: jwt的签发时间
                .setSubject(userName)        //sub(Subject)：代表这个JWT的主体，即它的所有人，这个是一个json格式的字符串，可以存放什么userid，roldid之类的，作为什么用户的唯一标志。
                .signWith(signatureAlgorithm, secretKey);//设置签名使用的签名算法和签名使用的秘钥
        //设置过期时间
        if(isMobile){
            if (JWT_EXPIRE_TIME_LONG_MOBILE >= 0) {
                long expMillis = nowMillis + JWT_EXPIRE_TIME_LONG_MOBILE;
                Date exp = new Date(expMillis);
                builder.setExpiration(exp);
            }
        }else{
            if (JWT_EXPIRE_TIME_LONG >= 0) {
                long expMillis = nowMillis + JWT_EXPIRE_TIME_LONG;
                Date exp = new Date(expMillis);
                builder.setExpiration(exp);
            }
        }

        String newToken = "jwt_" + builder.compact();
        //更新用户权限
        updateUserGrantedAuthority(userName);
        log.info("*** 创建Token ***  用户：{}，创建token：{}",userName,newToken);
        return newToken;
    }
    /**
     * 生成新token时，初始化token到Redis
     * @param userName
     * @param newToken
     */
    public void initNewToken(String userName, String newToken,boolean isMobile,boolean isLogin){

        String user_jwt_token_prefix = USER_JWT_TOKEN_PREFIX;           //Redis 存放 token 的 key 前缀
        long jwt_expire_time = JWT_EXPIRE_TIME;                         //Redis 存放 token 的 有效时间
        if (isMobile){
            user_jwt_token_prefix = USER_JWT_TOKEN_PREFIX_MOBILE;
            jwt_expire_time = JWT_EXPIRE_TIME_MOBILE;
        }

        //1.获取以前的token
        Object tokenObj = redisService.get(user_jwt_token_prefix + userName);
        String token = tokenObj != null ? tokenObj.toString() : "" ;
        if(StringUtils.isNotEmpty(token)){
            if(isLogin){
                //删除老的token
                redisService.del(token);
                log.info("*** 更新新Token到Redis *** 1.登录时，删除老Token： 用户：{}，老token：{}",userName,token);
            }else{
                //老token设置过期时间 OLD_TOKEN_EXPIRE 分钟（防止同一访问者 并发调用接口，前端无法及时存储新的token）
                redisService.set(token, userName, OLD_TOKEN_EXPIRE);
                log.info("*** 更新新Token到Redis *** 2.刷新token，设置老token过期时间： 用户：{}，老token：{}，有效时间：{}秒",userName,token, OLD_TOKEN_EXPIRE);
            }
        }

        //新token初始化
        redisService.set(newToken, userName, jwt_expire_time );
        redisService.set(user_jwt_token_prefix + userName, newToken, jwt_expire_time);

        Object o = redisService.get(user_jwt_token_prefix + userName);
        log.info("*** 更新新Token到Redis ***  3.用户：{}，是否来自移动端：{}，redis存放的新token：{},有效时间：{}秒",userName,isMobile,o ,jwt_expire_time);
        //更新 用户 基本缓存信息（查询过多，转为 其他地方更新）
        updateRedisUserInfoTo( userName,  newToken);
    }

    /**
     * 获取jwt的payload部分
     */
    public Claims getClaimFromToken(String token) {
        SecretKey secretKey = createSecretKey();
        return Jwts.parser()   //得到DefaultJwtParser
                .setSigningKey(secretKey)  //设置签名的秘钥
                .parseClaimsJws(token.replace("jwt_", ""))
                .getBody();
    }

    // 签名私钥
    private SecretKey createSecretKey(){
        byte[] encodedKey = Base64.decodeBase64(APPSECRET_KEY);//本地的密码解码
        SecretKey secretKey = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");// 根据给定的字节数组使用AES加密算法构造一个密钥，使用 encodedKey中的始于且包含 0 到前 leng 个字节这是当然是所有。（后面的文章中马上回推出讲解Java加密和解密的一些算法）
        return secretKey;
    }

    /**
     * 更新用户权限到redis
     * @param userName
     * @return
     */
    public boolean updateUserGrantedAuthority(String userName){
        SysUser user = sysUserService.findByUserName(userName);
        Collection<GrantedAuthority> authorities = new ArrayList<>();

        //如果 用户停用，返回默认个人权限
        if(user.getStatus() == 2){
            //设置用户权限 到redis
            String authoritiesJsonString = JSONObject.toJSONString(authorities);
            boolean b = redisService.set(JwtUtils.REDIS_USER_AUTHORITIES_PREFIX + user.getUserName(), authoritiesJsonString,JWT_EXPIRE_TIME);
            return b;
        }

        Set<String> userPermissionList = sysMenuService.userPermissionList(user.getId());
        userPermissionList.stream().forEach(f-> authorities.add(new SimpleGrantedAuthority(f)));
        String authoritiesJsonString = JSONArray.toJSONString(authorities);
        //设置用户权限 到redis
        boolean b = redisService.set(JwtUtils.REDIS_USER_AUTHORITIES_PREFIX + user.getUserName(), authoritiesJsonString,JWT_EXPIRE_TIME);
        return b;
    }

    /**
     * 从Redis获取用户权限
     * @param userName
     * @return
     */
    public Collection<GrantedAuthority> getUserAuthorityFromRedis(String userName){
        Object o = redisService.get(JwtUtils.REDIS_USER_AUTHORITIES_PREFIX + userName);
        try{
            Collection<GrantedAuthority> grantedAuthorities = JSONArray.parseArray(o.toString(), GrantedAuthority.class);
            return grantedAuthorities;
        }catch (Exception e){
            return null;
        }
    }

    /**
     * 更新 用户 基本缓存信息（查询过多，转为 其他地方更新）
     * @param userName
     * @param newToken
     * @return
     */
    public boolean updateRedisUserInfoTo(String userName, String newToken){
        UserInfo redisUserInfo = getRedisUserInfo(userName);
        if( null != redisUserInfo)return true;
        UserInfo userInfo =  sysUserService.getUserInfo(userName);
        if(null == userInfo)return false;
        boolean b = redisService.set(REDIS_USER_NAME_PREFIX + userName, userInfo);
        return b;
    }

    /**
     * 获取 用户 基本缓存信息
     * @param userName
     * @return
     */
    public UserInfo getRedisUserInfo(String userName){
        UserInfo userInfo = null;
        userInfo = (UserInfo)redisService.get(REDIS_USER_NAME_PREFIX + userName);
        return userInfo;
    }

    /**
     * 清空 用户token
     * @param userName
     */
    public void deleteUserToken(String userName){
        if(StringUtils.isNotBlank(userName)){
            redisService.del(USER_JWT_TOKEN_PREFIX + userName);
        }
    }

    /**
     * 清空用户token 和 用户信息
     * @param userName
     */
    public void clearUser(String userName){
        if(StringUtils.isNotBlank(userName)){
            String token = redisService.getAsString(USER_JWT_TOKEN_PREFIX + userName);
            redisService.del(USER_JWT_TOKEN_PREFIX + userName);
            redisService.del(token);

            String mobileToken = redisService.getAsString(USER_JWT_TOKEN_PREFIX_MOBILE + userName);
            redisService.del(USER_JWT_TOKEN_PREFIX_MOBILE + userName);
            redisService.del(mobileToken);

            redisService.del(REDIS_USER_NAME_PREFIX + userName);
            redisService.del(REDIS_USER_AUTHORITIES_PREFIX + userName);
        }
    }
}

